"""
Password Hasher
===============

Advanced password hashing utilities using bcrypt and Argon2.
Provides secure password hashing with salt generation and verification.
"""

import bcrypt
import logging
from typing import Optional, Tuple
import secrets
import hashlib

# Initialize logger
logger = logging.getLogger(__name__)

class PasswordHasher:
    """
    Advanced password hasher with multiple algorithms support.
    
    Features:
    - bcrypt hashing (default)
    - Argon2id hashing (optional)
    - Salt generation
    - Password strength validation
    - Hash migration support
    """
    
    def __init__(self, algorithm: str = 'bcrypt', rounds: int = 12):
        """
        Initialize password hasher.
        
        Args:
            algorithm: Hashing algorithm ('bcrypt' or 'argon2')
            rounds: Number of rounds for bcrypt (4-31)
        """
        self.algorithm = algorithm.lower()
        self.rounds = max(4, min(31, rounds))  # bcrypt rounds must be between 4-31
        
        if self.algorithm == 'argon2':
            try:
                import argon2
                self.argon2_hasher = argon2.PasswordHasher()
            except ImportError:
                logger.warning("argon2-cffi not installed, falling back to bcrypt")
                self.algorithm = 'bcrypt'
    
    def generate_salt(self) -> bytes:
        """
        Generate cryptographically secure salt.
        
        Returns:
            bytes: Generated salt
        """
        return bcrypt.gensalt(rounds=self.rounds)
    
    def hash_password(self, password: str) -> str:
        """
        Hash a password using the configured algorithm.
        
        Args:
            password: Plain text password
        
        Returns:
            str: Hashed password with algorithm identifier
        
        Raises:
            ValueError: If password is too weak
        """
        # Validate password strength
        self._validate_password_strength(password)
        
        if self.algorithm == 'bcrypt':
            return self._hash_bcrypt(password)
        elif self.algorithm == 'argon2':
            return self._hash_argon2(password)
        else:
            raise ValueError(f"Unsupported algorithm: {self.algorithm}")
    
    def _hash_bcrypt(self, password: str) -> str:
        """
        Hash password using bcrypt.
        
        Args:
            password: Plain text password
        
        Returns:
            str: bcrypt hash with algorithm prefix
        """
        salt = self.generate_salt()
        hash_bytes = bcrypt.hashpw(password.encode('utf-8'), salt)
        
        # Return format: $algorithm$hash
        return f"$bcrypt${hash_bytes.decode('utf-8')}"
    
    def _hash_argon2(self, password: str) -> str:
        """
        Hash password using Argon2id.
        
        Args:
            password: Plain text password
        
        Returns:
            str: Argon2 hash with algorithm prefix
        """
        hash_str = self.argon2_hasher.hash(password)
        return f"$argon2${hash_str}"
    
    def verify_password(self, password: str, hashed: str) -> bool:
        """
        Verify password against hash.
        
        Args:
            password: Plain text password
            hashed: Stored password hash
        
        Returns:
            bool: True if password matches
        """
        if not hashed or not password:
            return False
        
        # Extract algorithm and hash
        if hashed.startswith('$bcrypt$'):
            _, _, hash_part = hashed.partition('$bcrypt$')
            return self._verify_bcrypt(password, hash_part)
        elif hashed.startswith('$argon2$'):
            _, _, hash_part = hashed.partition('$argon2$')
            return self._verify_argon2(password, hash_part)
        else:
            # Legacy hash format - assume bcrypt without prefix
            return self._verify_bcrypt(password, hashed)
    
    def _verify_bcrypt(self, password: str, hash_part: str) -> bool:
        """
        Verify password using bcrypt.
        
        Args:
            password: Plain text password
            hash_part: bcrypt hash without prefix
        
        Returns:
            bool: True if password matches
        """
        try:
            return bcrypt.checkpw(password.encode('utf-8'), hash_part.encode('utf-8'))
        except Exception as e:
            logger.error(f"Error verifying bcrypt password: {str(e)}")
            return False
    
    def _verify_argon2(self, password: str, hash_part: str) -> bool:
        """
        Verify password using Argon2.
        
        Args:
            password: Plain text password
            hash_part: Argon2 hash without prefix
        
        Returns:
            bool: True if password matches
        """
        try:
            self.argon2_hasher.verify(hash_part, password)
            return True
        except Exception:
            return False
    
    def needs_rehash(self, hashed: str) -> bool:
        """
        Check if password hash needs to be updated to current settings.
        
        Args:
            hashed: Stored password hash
        
        Returns:
            bool: True if hash should be rehashed
        """
        if hashed.startswith('$bcrypt$'):
            # Extract bcrypt cost factor
            _, _, hash_part = hashed.partition('$bcrypt$')
            try:
                # bcrypt hash format: $2b$rounds$salt+hash
                parts = hash_part.split('$')
                if len(parts) >= 3:
                    current_rounds = int(parts[2])
                    return current_rounds != self.rounds
            except (ValueError, IndexError):
                return True
        
        return False
    
    def _validate_password_strength(self, password: str) -> None:
        """
        Validate password strength.
        
        Args:
            password: Password to validate
        
        Raises:
            ValueError: If password is too weak
        """
        if len(password) < 8:
            raise ValueError("Password must be at least 8 characters long")
        
        if len(password) > 128:
            raise ValueError("Password must not exceed 128 characters")
        
        if not any(c.islower() for c in password):
            raise ValueError("Password must contain at least one lowercase letter")
        
        if not any(c.isupper() for c in password):
            raise ValueError("Password must contain at least one uppercase letter")
        
        if not any(c.isdigit() for c in password):
            raise ValueError("Password must contain at least one digit")
        
        if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password):
            raise ValueError("Password must contain at least one special character")
        
        # Check for common weak passwords
        weak_passwords = {
            'password', '123456', '123456789', 'qwerty', 'abc123',
            'password123', 'admin', 'letmein', 'welcome', 'monkey'
        }
        
        if password.lower() in weak_passwords:
            raise ValueError("Password is too common and easily guessable")
        
        # Check for repeated characters
        if any(c * 3 in password for c in password):
            raise ValueError("Password should not contain repeated characters more than twice")
    
    def generate_secure_password(self, length: int = 12) -> str:
        """
        Generate a secure random password.
        
        Args:
            length: Password length (default: 12)
        
        Returns:
            str: Generated password
        """
        if length < 8:
            length = 8
        
        # Character sets
        lowercase = 'abcdefghijklmnopqrstuvwxyz'
        uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        digits = '0123456789'
        special = '!@#$%^&*(),.?":{}|<>'
        
        # Ensure at least one character from each set
        password = [
            secrets.choice(lowercase),
            secrets.choice(uppercase),
            secrets.choice(digits),
            secrets.choice(special)
        ]
        
        # Fill the rest with random characters from all sets
        all_chars = lowercase + uppercase + digits + special
        password.extend(secrets.choice(all_chars) for _ in range(length - 4))
        
        # Shuffle the password
        secrets.SystemRandom().shuffle(password)
        
        return ''.join(password)
    
    def hash_with_legacy_support(self, password: str, legacy_hash: Optional[str] = None) -> str:
        """
        Hash password with legacy hash migration support.
        
        Args:
            password: Plain text password
            legacy_hash: Legacy hash to verify against (optional)
        
        Returns:
            str: New hash if password is valid, or existing hash
        """
        # If legacy hash provided and password matches, return new hash
        if legacy_hash and self.verify_password(password, legacy_hash):
            return self.hash_password(password)
        
        # Otherwise, just hash the password
        return self.hash_password(password)

# Global password hasher instance
password_hasher = PasswordHasher()

def hash_password(password: str) -> str:
    """
    Convenience function to hash a password.
    
    Args:
        password: Plain text password
    
    Returns:
        str: Hashed password
    """
    return password_hasher.hash_password(password)

def verify_password(password: str, hashed: str) -> bool:
    """
    Convenience function to verify a password.
    
    Args:
        password: Plain text password
        hashed: Stored password hash
    
    Returns:
        bool: True if password matches
    """
    return password_hasher.verify_password(password, hashed)